﻿namespace V8Bridge.FSharp

open System
open System.Diagnostics
open System.Collections.Concurrent
open System.ComponentModel
open System.Threading
open System.Security.Permissions

[<Serializable>]
type internal WorkItem(action: SendOrPostCallback, state: obj) =
    let mre = new ManualResetEventSlim(false)

    member x.Run() =
        use mre = mre
        action.Invoke(state)
        mre.Set()

    /// Blocks the current thread until the the action has been run.
    member x.WaitForCompletion() =
        // If disposed, ManualResetEventSlim throws ObjectDisposedException from Wait() but not from IsSet, so we check IsSet first
        // to avoid the cost of generating a stack trace and such when the exception is thrown and ignored.
        try
            if not mre.IsSet then
                mre.Wait()
        with :? ObjectDisposedException -> ()

    interface IDisposable with
        member x.Dispose() =
            mre.Dispose()

/// Because V8 can only ever be called from the specific thread that created it,
/// we use this custom synchronization context to control on which thread
/// code in an async workflow executes, by way of Async.SwitchToContext.
[<SecurityPermission(SecurityAction.Demand, ControlThread = true)>]
type AffinitySyncContext() as this =
    inherit SynchronizationContext()

    let actionQueue = ConcurrentQueue<WorkItem>()
    let mutable exitLoop = 0
    let dispose (d:#IDisposable) = d.Dispose()
    let rec clearQueue () =
        match actionQueue.TryDequeue() with
        | true, item ->
            dispose item
            clearQueue()
        | false, _ -> ()

    let thread = 
        let workLoop () =
            Debug.Assert(Object.ReferenceEquals(SynchronizationContext.Current, null))
            SynchronizationContext.SetSynchronizationContext(this)
            while exitLoop = 0 do
                match actionQueue.TryDequeue() with
                | true, item -> item.Run()
                | false, _   -> Thread.Sleep 100
        let thread = Thread(workLoop, IsBackground = true, Name = "Affinity Context Thread")
        thread.Start()
        thread

    member this.Close() =
        if exitLoop = 0 then
            Interlocked.Exchange(&exitLoop, 1) |> ignore
            if not <| thread.Join(TimeSpan.FromSeconds(3.0)) then
                thread.Abort()
            clearQueue()

    override this.CreateCopy() =
        upcast this

    override this.Post(callback, state) =
        let workItem = new WorkItem(callback, state)
        actionQueue.Enqueue(workItem)

    override this.Send(callback, state) =
        // If already on the correct context, must invoke now to avoid deadlock.
        if Object.ReferenceEquals(SynchronizationContext.Current, this) then
            callback.Invoke(state)
        else
            use workItem = new WorkItem(callback, state)
            actionQueue.Enqueue(workItem)
            workItem.WaitForCompletion()

    interface IDisposable with
        member x.Dispose() = x.Close()
        
